frida常见使用及脚本收集

您所在的位置:网站首页 frida 枚举 so frida常见使用及脚本收集

frida常见使用及脚本收集

2023-07-10 13:08| 来源: 网络整理| 查看: 265

启动123456789adb push xxx.apk /sdcard/xxx.apkadb forward tcp:27042 tcp:27042adb forward tcp:27043 tcp:27043adb shellsucd /data/local/tmp./frida-server frida -U -l .\t1.js -f demo2.jni.com.myapplication 调试add 调试参数

在xml里面加上

1android:debuggable="true"

img

调试init_array和JNI_Onload

用frida spawn,加pause参数(16版本就–pause,14及以前的版本就不用加)。然后用ida attach后运行,frida命令行输入%resume后点击same就可以调试上,且可以调试init_array和JNI_Onload这些位置的函数

QBDI1setenforce 0 问题大全logcat中文乱码1chcp 65001 kill frida-server12netstat -tunlpkill -9 {pid} frida检测 遍历运行的进程列表从而检查fridaserver是否在运行 遍历data/local/tmp目录查看有无frida相关文件 遍历映射库,检测frida相关so 检查27042这个默认端口 内存中扫描frida特征字符串 “LIBFRIDA” frida使用D-Bus协议通信,我们为每个开放的端口发送D-Bus的认证消息 检测有无frida特征的线程名,如:gum-js-loop 简单绕过检测 frida端口改成其他的数,比如1234 12adb shell su -c "/data/local/tmp/hs1604 -l 0.0.0.0:1234 &"frida -H 127.0.0.1:1234 -f xxx -l xxx.js android_server同样将端口改成别的 1./as64 -p 12345 frida-server和android_server的名字不要直接写本名,改成as,fs这种 用hluwa_server代替frida_server server位置不要放在data/local/tmp,可以放在data/目录下 脚本python脚本加载脚本1234567891011import timeimport fridadevice = frida.get_remote_device()pid = device.spawn(["com.kanxue.pediy1"])#程序名device.resume(pid)time.sleep(1)session = device.attach(pid)with open("s1.js") as f: script = session.create_script(f.read())script.load()input() 获取最前端进程1234import fridardev = frida.get_remote_device()front_app = rdev.get_frontmost_application()print(front_app) 基础js脚本枚举所有的类enumerateLoadedClasses

这个函数拥有两个回调函数onMatch和onComplete,其中onMatch的参数就是类的信息。这个函数会遍历所有类,所以我们如果再onMatch处输出其参数就可以获得所有类的信息

12345678910111213setTimeout(function (){ Java.perform(function (){ console.log("n[*] enumerating classes..."); Java.enumerateLoadedClasses({ onMatch: function(_className){ console.log("[*] found instance of '"+_className+"'"); }, onComplete: function(){ console.log("[*] class enuemration complete"); } }); });});

Java.enumerateLoadedClassesSync()也能实现这个功能,不过它返回的是数组

显示安卓系统版本号1234567891011121314function frida_Java() { Java.perform(function () { //作为判断用 if(Java.available) { //注入的逻辑代码 console.log("",Java.androidVersion); }else{ //未能正常加载JAVA VM console.log("error"); } });} setImmediate(frida_Java,0); 枚举类加载器Java.enumerateClassLoaders

该api枚举的是java vm中的类加载器,也是俩回调函数,onMatch和onComplete,其中onMatch的参数是loader,因为是枚举类加载器嘛。

1234567891011121314151617181920212223function frida_Java() { Java.perform(function () { if(Java.available) { //枚举当前加载的Java VM类加载器 Java.enumerateClassLoaders({ //回调函数,参数loader是类加载的信息 onMatch: function (loader) { console.log("",loader); }, //枚举完毕所有类加载器之后的回调函数 onComplete: function () { console.log("end"); } }); }else{ console.log("error"); } });} setImmediate(frida_Java,0);

image-20221226000125286

附加调用Java.perform

该api很重要,java.perform(fn)主要用于当前线程附加到java vm并调用fn方法。

1234567891011121314function frida_Java() { //运行当前js脚本时会对当前线程附加到Java VM虚拟机,并且执行function方法 Java.perform(function () { //判断是否Java VM正常运行 if(Java.available) { //如不意外会直接输出 hello console.log("hello"); }else{ console.log("error"); } });} setImmediate(frida_Java,0);

也有个兄弟Java.performNow(fn)

获取类Java.use

Java.use(classname),可以动态获取classname的定义,然后对其调用$new()可以调用构造函数,来从中实例化对象,想要回收类的时候可以调用$Dispose()方法显式释放

1234567891011Java.perform(function () { //获取android.app.Activity类 var Activity = Java.use('android.app.Activity'); //获取java.lang.Exception类 var Exception = Java.use('java.lang.Exception'); //拦截Activity类的onResume方法 Activity.onResume.implementation = function () { //调用onResume方法的时候,会在此处被拦截并且调用以下代码抛出异常! throw Exception.$new('Oh noes!'); };}); 扫描实例类Java.choose

堆上找实例化的对象

12345678910111213Java.perform(function () { //查找android.view.View类在堆上的实例化对象 Java.choose("android.view.View", { //枚举时调用 onMatch:function(instance){ //打印实例 console.log(instance); }, //枚举完成后调用 onComplete:function() { console.log("end") }});}); 类型转换器Java.cast

将指定变量转换成需要的类型。通常在拦截so层时会用此函数将jstring,jarray转换后查看其值

定义数组Java.array

js中定义java数组的api

1234567891011Java.perform(function () { //定义一个int数组、值是1003, 1005, 1007 var intarr = Java.array('int', [ 1003, 1005, 1007 ]); //定义一个byte数组、值是0x48, 0x65, 0x69 var bytearr = Java.array('byte', [ 0x48, 0x65, 0x69 ]); for(var i=0;i 0) { /* do something with this.fileDescriptor */ } }});

通过我们对Interceptor.attach函数有一些基本了解了~它还包含一些属性。

索引 属性 含义 1 returnAddress 返回地址,类型是NativePointer 2 context 上下文:具有键pc和sp的对象,它们是分别为ia32/x64/arm指定EIP/RIP/PC和ESP/RSP/SP的NativePointer对象。其他处理器特定的键也可用,例如eax、rax、r0、x0等。也可以通过分配给这些键来更新寄存器值。 3 errno 当前errno值 4 lastError 当前操作系统错误值 5 threadId 操作系统线程ID 6 depth 相对于其他调用的调用深度 12345678910111213141516171819202122function frida_Interceptor() { Java.perform(function () { //对So层的导出函数getSum进行拦截 Interceptor.attach(Module.findExportByName("libhello.so" , "Java_com_roysue_roysueapplication_hellojni_getSum"), { onEnter: function(args) { //输出 console.log('Context information:'); //输出上下文因其是一个Objection对象,需要它进行接送、转换才能正常看到值 console.log('Context : ' + JSON.stringify(this.context)); //输出返回地址 console.log('Return : ' + this.returnAddress); //输出线程id console.log('ThreadId : ' + this.threadId); console.log('Depth : ' + this.depth); console.log('Errornr : ' + this.err); }, onLeave:function(retval){ } }); });}setImmediate(frida_Interceptor,0);

img

Interceptor.detachAll

让attach附加拦截的回调函数失效

Interceptor.replace

替换自己实现的函数

1234567891011121314151617function frida_Interceptor() { Java.perform(function () { //这个c_getSum方法有两个int参数、返回结果为两个参数相加 //这里用NativeFunction函数自己定义了一个c_getSum函数 var add_method = new NativeFunction(Module.findExportByName('libhello.so', 'c_getSum'), 'int',['int','int']); //输出结果 那结果肯定就是 3 console.log("result:",add_method(1,2)); //这里对原函数的功能进行替换实现 Interceptor.replace(add_method, new NativeCallback(function (a, b) { //h不论是什么参数都返回123 return 123; }, 'int', ['int', 'int'])); //再次调用 则返回123 console.log("result:",add_method(1,2)); });} new NativePointer(s)

nativePointer对象相当于指针

12345678910111213141516171819function frida_NativePointer() { Java.perform(function () { //第一种字符串定义方式 十进制的100 输出为十六进制0x64 const ptr1 = new NativePointer("100"); console.log("ptr1:",ptr1); //第二种字符串定义方式 直接定义0x64 同等与定义十六进制的64 const ptr2 = new NativePointer("0x64"); console.log("ptr2:",ptr2); //第三种定数值义方式 定义数字int类型 十进制的100 是0x64 const ptr3 = new NativePointer(100); console.log("ptr3:",ptr3); });} setImmediate(frida_NativePointer,0);输出如下,都会自动转为十六进制的0x64ptr1: 0x64ptr2: 0x64ptr3: 0x64 运算符和指针读写api

img

img

下脚本是readByteArray示例

12345678910function frida_NativePointer() { Java.perform(function () { console.log(""); //拿到libc.so在内存中的地址 var pointer = Process.findModuleByName("libc.so").base; //读取从pointer地址开始的16个字节 console.log(pointer.readByteArray(0x10)); });} setImmediate(frida_NativePointer,0);

输出就是libc.so的前16个字节

readPointer()

定义是从此内存位置读取NativePointer

12345var pointer = Process.findModuleByName("libc.so").base;console.log(pointer.readByteArray(0x10));console.log("readPointer():"+pointer.readPointer());输出如下。readPointer():0x464c457f writePointer(ptr)

读取ptr指针地址到当前指针

1234567891011121314151617 //先打印pointer指针地址 console.log("pointer :"+pointer); //分配四个字节的空间地址 const r = Memory.alloc(4); //将pointer指针写入刚刚申请的r内 r.writePointer(pointer); //读取r指针的数据 var buffer = Memory.readByteArray(r, 4); //r指针内放的pointer指针地址 console.log(buffer);输出如下。//console.log("pointer :"+pointer); 这句打印的地址 也就是libc的地址pointer :0xf588f000//console.log(buffer); 输出buffer 0xf588f000在内存数据会以00 f0 88 f5方式显示 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF00000000 00 f0 88 f5 .... readS32()、readU32()

从该内存位置读取有符号或无符号8/16/32/etc或浮点数/双精度值,并将其作为数字返回。这里拿readS32()、readU32()作为演示.

1234567891011 //从pointer地址读4个字节 有符号 console.log(pointer.readS32()); //从pointer地址读4个字节 无符号 console.log(pointer.readU32());输出如下。 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF00000000 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 .ELF............1179403647 == 0x464c457f1179403647 == 0x464c457f writeS32()、writeU32()

将有符号或无符号8/16/32/等或浮点数/双精度值写入此内存位置。

12345678910 //申请四个字节的内存空间 const r = Memory.alloc(4); //将0x12345678写入r地址中 r.writeS32(0x12345678); //输出 console.log(r.readByteArray(0x10));// writeS32()、writeU32()输出的也是一样的,只是区别是有符号和无符号输出如下。 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF00000000 78 56 34 12 00 00 00 00 00 00 00 00 00 00 00 00 xV4............. readByteArray(length))、writeByteArray(bytes)

readByteArray(length))连续读取内存length个字节,、writeByteArray连续写入内存bytes。

1234567891011121314151617181920 //先定义一个需要写入的字节数组 var arr = [ 0x72, 0x6F, 0x79, 0x73, 0x75, 0x65]; //这里申请以arr大小的内存空间 const r = Memory.alloc(arr.length); //将arr数组字节写入r Memory.writeByteArray(r,arr); //读取arr.length大小的数组 var buffer = Memory.readByteArray(r, arr.length); console.log("Memory.readByteArray:"); console.log(hexdump(buffer, { offset: 0, length: arr.length, header: true, ansi: false }));输出如下。 Memory.readByteArray: 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF00000000 72 6f 79 73 75 65 roysue readCString([size = -1])、writeUtf8String(str)

readCString功能是读取指针地址位置的字节字符串,对应的writeUtf8String是写入指针地址位置的字符串处。(这里的r是接着上面的代码的变量)。

123456//在这里直接使用readCString读取会把上面的'roysue'字符串读取出来console.log("readCString():"+r.readCString());//这里是写入字符串 也就是 roysue起始位置开始被替换为hahaconst newPtrstr = r.writeUtf8String("haha");//替换完了之后再继续输出 必然是hahaconsole.log("readCString():"+newPtrstr.readCString()); NativeFunction

创建新的NativeFunction以调用address处的函数(用NativePointer指定),其中return Type指定返回类型,argTypes数组指定参数类型。如果不是系统默认值,还可以选择指定ABI。对于可变函数,添加一个‘.’固定参数和可变参数之间的argTypes条目,我们来看看官方的例子。

12345678// LargeObject HandyClass::friendlyFunctionName();//创建friendlyFunctionPtr地址的函数var friendlyFunctionName = new NativeFunction(friendlyFunctionPtr, 'void', ['pointer', 'pointer']);//申请内存空间 var returnValue = Memory.alloc(sizeOfLargeObject);//调用friendlyFunctionName函数friendlyFunctionName(returnValue, thisPtr);

函数定义格式为new NativeFunction(address, returnType, argTypes[, options]),参照这个格式能够创建函数并且调用!returnType和argTypes[,]分别可以填void、pointer、int、uint、long、ulong、char、uchar、float、double、int8、uint8、int16、uint16、int32、uint32、int64、uint64这些类型,根据函数的所需要的type来定义即可。

在定义的时候必须要将参数类型个数和参数类型以及返回值完全匹配,假设有三个参数都是int,则new NativeFunction(address, returnType, ['int', 'int', 'int']),而返回值是int则new NativeFunction(address, 'int', argTypes[, options]),必须要全部匹配,并且第一个参数一定要是函数地址指针。

NativeCallback对象

new NativeCallback(func,rereturn Type,argTypes[,ABI]):创建一个由JavaScript函数func实现的新NativeCallback,其中return Type指定返回类型,argTypes数组指定参数类型。您还可以指定ABI(如果不是系统默认值)。有关支持的类型和Abis的详细信息,请参见NativeFunction。注意,返回的对象也是一个NativePointer,因此可以传递给Interceptor#replace。当将产生的回调与Interceptor.replace()一起使用时,将调用func,并将其绑定到具有一些有用属性的对象,就像Interceptor.Attach()中的那样。我们来看一个例子。如下,利用NativeCallback做一个函数替换。

12345678910Java.perform(function () { var add_method = new NativeFunction(Module.findExportByName('libhello.so', 'c_getSum'), 'int',['int','int']); console.log("result:",add_method(1,2)); //在这里new一个新的函数,但是参数的个数和返回值必须对应 Interceptor.replace(add_method, new NativeCallback(function (a, b) { return 123; }, 'int', ['int', 'int'])); console.log("result:",add_method(1,2));}); js脚本收集ssl unpinning(okhttp4.9.1)12345678910111213141516171819202122// okhttp4try { var CertificatePinner = Java.use('okhttp3.CertificatePinner'); CertificatePinner.check.overload('java.lang.String', 'java.util.List').implementation = function(str) { writeFile('! Intercepted okhttp4 in [check()]: ' + str); return; }; try {//.overload('java.lang.String', 'kotlin.jvm.functions.Function0') CertificatePinner.check$okhttp.implementation = function(str, _) { writeFile('! Intercepted okhttp4 in [check$okhttp]: ' + str); return; }; } catch (ex) { writeFile("is this Okhttp3 ?!"); } writeFile('* Setup okhttp4 pinning')} catch (err) { writeFile('* Unable to hook into okhttp4 pinner') writeFile(err);} hook dlopen123456789101112131415161718192021222324252627function hook_dlopen() { var dlopenAddr = Module.findExportByName(null, "dlopen"); if (dlopenAddr == null) { dlopenAddr = Module.findExportByName(null, "android_dlopen_ext"); } Interceptor.attach(dlopenAddr, { onEnter: function (args) { var pathptr = args[0]; if (pathptr !== undefined && pathptr != null) { var path = ptr(pathptr).readCString(); console.log("[dlopen]", path); this.can_hook_lib = false; this.can_hook_libmono = false; if (path.indexOf("libil2cpp.so") >= 0) { this.can_hook_lib = true; } } }, onLeave: function (retval) { if (this.can_hook_lib) { var libso_address = Module.findBaseAddress("libil2cpp.so") hook_input(libso_address) } } })} hook exit1234var sysexit = Java.use("java.lang.System");sysexit.exit.implementation = function(a0){ console.log("bypass exit");} hook pthread_create123456789101112131415161718function pthread_hook(){ //int pthread_create(pthread_t *newthread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg) var pthread_create_origin = Module.findExportByName("libc.so","pthread_create"); var pthread_create_replace = new NativeFunction(pthread_create_origin,"int",["pointer","pointer","pointer","pointer"]); Interceptor.replace(pthread_create_origin,new NativeCallback(function(ptr0,ptr1,ptr2,ptr3){ var retval = ptr(0);//?why if(ptr1.isNull() && ptr3.isNull()){ send("bypass the pthread"); } else{ retval = pthread_create_replace(ptr0,ptr1,ptr2,ptr3); } send("pthread execute next is print_secert"); print_secert(); return retval; },"int",["pointer","pointer","pointer","pointer"]));}//本代码是判断第二个和第四个参数为0的时候hook,用的时候看自己的需求 stalker简单使用12345678910111213141516171819202122232425262728293031323334353637383940Interceptor.attach(tatgetAddr, { onEnter: function(args){ console.log("enter tatgetAddr"); this.pid = Process.getCurrentThreadId(); Stalker.follow(this.pid,{ events:{ call:false,//不打印这些汇编 ret:false, exec:false, block:false, compile:false }, onReceive:function(events){ }, transform: function (iterator) {//主要是transform var instruction = iterator.next(); const startAddress = instruction.address; if(count {}) } }); 打印调用栈的log1console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));

可以输出调用栈

找interface的实现

image-20230118140334167

hook onclick

可以加上上方的打印调用栈的代码

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253var jclazz = null;var jobj = null;function getObjClassName(obj) { if (!jclazz) { var jclazz = Java.use("java.lang.Class"); } if (!jobj) { var jobj = Java.use("java.lang.Object"); } return jclazz.getName.call(jobj.getClass.call(obj));}function watch(obj, mtdName) { var listener_name = getObjClassName(obj); var target = Java.use(listener_name); if (!target || !mtdName in target) { return; } // send("[WatchEvent] hooking " + mtdName + ": " + listener_name); target[mtdName].overloads.forEach(function (overload) { overload.implementation = function () { //send("[WatchEvent] " + mtdName + ": " + getObjClassName(this)); console.log("[WatchEvent] " + mtdName + ": " + getObjClassName(this)) return this[mtdName].apply(this, arguments); }; })}function OnClickListener() { Java.perform(function () { Java.use("android.view.View").setOnClickListener.implementation = function (listener) { if (listener != null) { watch(listener, 'onClick'); } return this.setOnClickListener(listener); }; Java.choose("android.view.View$ListenerInfo", { onMatch: function (instance) { instance = instance.mOnClickListener.value; if (instance) { console.log("mOnClickListener name is :" + getObjClassName(instance)); watch(instance, 'onClick'); } }, onComplete: function () { } }) })}setImmediate(OnClickListener); hook linker1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677var mprotect_cnt = 0//frida -U --no-pause -f com.testlinker.ty -l hook.jsfunction sleep(delay) { var start = (new Date()).getTime(); while ((new Date()).getTime() - start < delay) { continue; }} function hook_svc_mprotect() { let base_svc_mprotect = Module.findBaseAddress("libSecShell.so"); if (base_svc_mprotect != null) { console.log("base_svc_mprotect : " + base_svc_mprotect) }else{ return ; } let svc_mprotect = base_svc_mprotect.add(0xC0778);//32位 Interceptor.attach(svc_mprotect, { onEnter: function(args) { console.log("==========================================") console.log("svc_mprotect: start = " + args[0] + " , len = " + args[1] + " , ATTRIBUTES = " + args[2]) mprotect_cnt += 1 console.log(hexdump(base_svc_mprotect.add(0x281B4))) }, onLeave: function(){ console.log("svc_mprotect leave") console.log("==========================================") } })}function dis(address, number) { for (var i = 0; i < number; i++) { var ins = Instruction.parse(address); console.log("address:" + address + "--dis:" + ins.toString()); address = ins.next; }}//libc->strstr() 从linker里面找到call_function的地址function hook() {//call_function("DT_INIT", init_func_, get_realpath()); var linkermodule if (Process.pointerSize == 4) { linkermodule = Process.findModuleByName("linker"); }else if (Process.pointerSize == 8) { linkermodule = Process.findModuleByName("linker64"); } // var linkermodule = Process.getModuleByName("linker"); var call_function_addr = null; var symbols = linkermodule.enumerateSymbols(); for (var i = 0; i < symbols.length; i++) { var symbol = symbols[i]; //LogPrint(linkername + "->" + symbol.name + "---" + symbol.address); if (symbol.name.indexOf("__dl__ZL13call_functionPKcPFviPPcS2_ES0_") != -1) { call_function_addr = symbol.address; //LogPrint("linker->" + symbol.name + "---" + symbol.address) } } Interceptor.attach(call_function_addr, { onEnter: function (args) { var type = ptr(args[0]).readUtf8String(); var address = args[1]; var sopath = ptr(args[2]).readUtf8String(); console.log("loadso:" + sopath + "--addr:" + address + "--type:" + type); if (sopath.indexOf("libSecShell.so") != -1) { var libnativemodule = Process.getModuleByName("libSecShell.so");//call_function正在加载目标so,这时就拦截下来 var base = libnativemodule.base; hook_svc_mprotect() } } })}function main() { hook();}setImmediate(main) frida指令收集adb指令12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364# 查看当前连接设备adb devices# 多个设备时指定设备adb -s 设备号 其他指令# adb -s 127.0.0.1:21503 shell# adb -s FA6AE0309067 shell# 127.0.0.1:5555 蓝叠# 127.0.0.1:7555 MUMU模拟器# 127.0.0.1:62001 夜游神模拟器# 127.0.0.1:21503 逍遥模拟器# 查看Android处理器架构adb shell getprop ro.product.cpu.abi# 安装APPadb install xxx.apk# 安装APP,已经存在,覆盖安装adb install -r xxx.apk# 卸载APPadb uninstall app包名# 卸载APP,保留数据adb uninstall -k app包名# 往手机传递文件adb push 文件名 手机路径# 从手机端获取文件adb pull 手机路径/文件名# 移动文件mv /sdcard/Download/frida-server-12.8.0 /data/local/tmp/# 修改文件权限chmod 777 /data/local/tmp/frida-server-12.8.0# 查看日志adb logcat# 清日志adb logcat -c# 手机端安装的所有app包名adb shell pm list packages# 查看当前包名和主Activityadb shell dumpsys window | findstr mCurrentFocus# 启动APPadb shell am start 包名/主Activity# adb shell am start com.autonavi.minimap/com.autonavi.map.activity.NewMapActivity# 关闭Appadb shell am force-stop 包名# adb shell am force-stop com.autonavi.minimap# 屏幕截图adb shell screencap 手机路径/xxx.png# 录制视频adb shell screenrecord 手机路径/xxx.mp4 frida命令1234567891011121314151617181920212223242526272829303132# 启动frida-server(模拟器)./data/local/tmp/frida-server-12.8.0-android-x86# 启动frida-server(Pixel真机)./data/local/tmp/frida-server-12.8.0-android-arm64# 转发端口adb forward tcp:27042 tcp:27042adb forward tcp:27043 tcp:27043# 列举出来所有连接到电脑上的设备frida-ls-devices# 连接到指定设备frida-ps -D tcp# 列举出来设备上的所有进程frida-ps -U# 列举出来设备上的所有应用程序frida-ps -Ua# 列举出来设备上的所有已安装应用程序和对应的名字frida-ps -Uai# 跟踪某个函数frida-trace -U -f Name -i "函数名"# frida-trace -U -f com.autonavi.minimap -i "getRequestParams"# 跟踪某个方法frida-trace -U -f Name -m "方法名"# frida-trace -U -f com.autonavi.minimap -m "MapLoader"


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3